package com.binance.feign.ribbon.policy;

import com.netflix.loadbalancer.Server;
import com.netflix.loadbalancer.ServerStats;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryContext;
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryPolicy;
import org.springframework.cloud.client.loadbalancer.ServiceInstanceChooser;
import org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient;
import org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerContext;
import org.springframework.http.HttpMethod;

import java.net.ConnectException;
import java.util.ArrayList;
import java.util.List;

/**
 * Created by Fei.Huang on 2018/9/7.
 * <p>
 * 微服务中调用retry的问题：
 * 1.Feign相关的重试有这两种，Feign自身的重试、ribbon的重试。
 * 2.当前微服务项目中，默认Feign自身的重试关闭了，手动配置的是ribbon重试。
 * 3.spring-cloud ribbon底层是依赖了spring-retry。
 * 4.spring-retry的逻辑，主要是判断是否是GET请求，如果是GET请求则可以进行重试。
 * 5.BNB项目中由于没有合理应用HTTP规范，大多请求都是POST，所以基于spring-retry的重试基本不起作用。
 * 6.BNB项目中需要重试的场景：
 * 6.1.当ConnectException时(例如Connection Refused)，不管是GET还是POST都应该retry，并且是retryNextServer。
 * 6.2.当SocketTimeoutException时，GET应该retry，retry当前server一两次，看情况是否继续retryNextServer，而POST不应该retry。
 */
@Log4j2
public class RibbonLoadBalancedRetryPolicy implements LoadBalancedRetryPolicy {

    private int sameServerCount = 0;
    private int nextServerCount = 0;
    private String serviceId;
    private RibbonLoadBalancerContext lbContext;
    private ServiceInstanceChooser loadBalanceChooser;
    List<Integer> retryableStatusCodes = new ArrayList<>();

    public RibbonLoadBalancedRetryPolicy(String serviceId, RibbonLoadBalancerContext context, ServiceInstanceChooser loadBalanceChooser) {
        this.serviceId = serviceId;
        this.lbContext = context;
        this.loadBalanceChooser = loadBalanceChooser;
    }

    private boolean canRetry(LoadBalancedRetryContext context) {
        HttpMethod method = context.getRequest().getMethod();

        // 对GET请求，都进行重置
        if (HttpMethod.GET == method) {
            return true;
        }

        // 对非GET请求，只对ConnectException重试
        if (HttpMethod.GET != method && (context.getLastThrowable() instanceof ConnectException)) {
            return true;
        }

        return false;
    }

    @Override
    public boolean canRetrySameServer(LoadBalancedRetryContext context) {
        HttpMethod method = context.getRequest().getMethod();

        boolean canRetrySameServer;

        // ConnectException 不RetrySameServer，直接RetryNextServer
        if (null != context.getLastThrowable() && (context.getLastThrowable() instanceof ConnectException)) {
            canRetrySameServer = false;
        } else {
            canRetrySameServer = sameServerCount < lbContext.getRetryHandler().getMaxRetriesOnSameServer() && canRetry(context);
        }

        log.info("feignRibbon canRetrySameServer={}, method:{}, sameServerCount:{}, exception:{}", canRetrySameServer, method, sameServerCount, context.getLastThrowable());
        return canRetrySameServer;
    }

    @Override
    public boolean canRetryNextServer(LoadBalancedRetryContext context) {
        HttpMethod method = context.getRequest().getMethod();
        boolean canRetryNextServer = nextServerCount <= lbContext.getRetryHandler().getMaxRetriesOnNextServer() && canRetry(context);
        log.info("feignRibbon canRetryNextServer={}, method:{}, nextServerCount:{}, exception:{}", canRetryNextServer, method, nextServerCount, context.getLastThrowable());
        return canRetryNextServer;
    }

    @Override
    public void close(LoadBalancedRetryContext context) {

    }

    @Override
    public void registerThrowable(LoadBalancedRetryContext context, Throwable throwable) {
        if (lbContext.getRetryHandler().isCircuitTrippingException(throwable)) {
            updateServerInstanceStats(context);
        }

        if (!canRetrySameServer(context) && canRetryNextServer(context)) {
            String previousServer = getServerInfo(context);
            context.setServiceInstance(loadBalanceChooser.choose(serviceId));
            String newServer = getServerInfo(context);
            log.info("newServer selected, newServer:{}, previousServer:{}, retryCount:{}", newServer, previousServer, context.getRetryCount());
        }
        if (sameServerCount >= lbContext.getRetryHandler().getMaxRetriesOnSameServer() && canRetry(context)) {
            sameServerCount = 0;
            nextServerCount++;
            if (!canRetryNextServer(context)) {
                context.setExhaustedOnly();
            }
        } else {
            sameServerCount++;
        }
    }

    private String getServerInfo(LoadBalancedRetryContext context) {
        return null != context.getServiceInstance() ? context.getServiceInstance().getHost() + ":" + context.getServiceInstance().getPort() : StringUtils.EMPTY;
    }

    private void updateServerInstanceStats(LoadBalancedRetryContext context) {
        ServiceInstance serviceInstance = context.getServiceInstance();
        if (serviceInstance instanceof RibbonLoadBalancerClient.RibbonServer) {
            Server lbServer = ((RibbonLoadBalancerClient.RibbonServer) serviceInstance).getServer();
            ServerStats serverStats = lbContext.getServerStats(lbServer);
            serverStats.incrementSuccessiveConnectionFailureCount();
            serverStats.addToFailureCount();
            log.info(lbServer.getHostPort() + " RetryCount: " + context.getRetryCount()
                    + " Successive Failures: " + serverStats.getSuccessiveConnectionFailureCount()
                    + " CircuitBreakerTripped:" + serverStats.isCircuitBreakerTripped());
        }
    }

    @Override
    public boolean retryableStatusCode(int statusCode) {
        return retryableStatusCodes.contains(statusCode);
    }
}